<?php

/***  Documentation

    Greqo Blogger Class
    Last Update: $Date: 2009-05-23 21:10:24 +0000 (Sat, 23 May 2009) $
    Author: Tom at klenwell@gmail.com

DESCRIPTION
    An extension of the Greqo base class for Google's Blogger API
  
USAGE    
    // authorization using GreqoBlogger extension
    $GreqoBlogger = new GreqoBlogger();
    $GreqoBlogger->email = 'yourname@gmail.com';
    $GreqoBlogger->password = '*******';
    if ( !$auth_token = $GreqoBlogger->request_clientlogin_token($service='blogger', $force=1) ) 
      die('unable to get auth token');

NOTES
    Supports the following query parameters:
        
        alt: atom, rss, json
        category: array (aka label)
        start-index: int
        *offset: int
        max-results: int
        *limit: int
        orderby: lastmodified, starttime, updated
        published-min: datetime
        published-max: datetime
        *between: array(start-datetime, end-datetime)
        updated-min: datetime
        updated-max: datetime    

    * indicates non-standard Google parameter offered for convenience.
        
REFERENCE
    Google Data API Query Parameters
        http://code.google.com/apis/gdata/docs/2.0/reference.html#Queries
    
______________________________________________________________________________*/

// load base Greqo class
require_once('greqo.class.php');

class GreqoBlogger extends Greqo
{   
    // service settings
    var $service = 'blogger';
    var $gdata_version = '1.0';
    var $service_url = 'http://www.blogger.com/feeds/default/blogs';
    var $blog_id = 0;
    var $content_type = 'html';        
    
    // service data
    var $BlogList = array();
     
    // private
    var $_filename = '';
    var $_dirpath = '';


    /* MAGIC METHODS */
    // php4 constructor
    function GreqoBlogger($start_session=1)
    {
        $this->__construct($start_session);
        register_shutdown_function( array($this, '__destruct') );
    }


    // php5 constructor
    function __construct($ss)
    {       
        // load parent class
        parent::__construct($ss); 
    }
    
    
    // destructor
    function __destruct()
    {
        $this->debug(sprintf("%s END\n\n", __CLASS__));
    }
  

    /* ** PUBLIC METHODS ** */
    
    /* Read Methods */
    function request_blog_list($profile_id='default')
    {
        $method = HTTP_REQUEST_METHOD_GET;
        $urlt_ = 'http://www.blogger.com/feeds/%s/blogs';
        $request_url = sprintf($urlt_, $profile_id);
        
        // prepare request
        if ( !$this->_prepare_request($request_url, $method) )
            return 0;
        
        // send request
        if ( !$this->_send_client_request() )
            return 0;  
        
        // process response
        if ( $this->GreqoResponse->code_class == 2 )
            $this->BlogList = $this->atom_to_array($this->GreqoResponse->body);
        else
            return $this->_trigger_request_error( sprintf(
                'Failed Request [%s]: %s',
                $this->GreqoResponse->code,
                htmlspecialchars($this->GreqoResponse->body)
            ));
        
        // add ID array to BlogList array
        $this->BlogList['ID'] = array();
        foreach ( $this->BlogList['ITEM'] as $i => $BlogInfo )
        {
            $blog_id = $this->_parse_blog_id($BlogInfo['id']);
            $user_id = $this->_parse_user_id($BlogInfo['id']);
            $this->BlogList['ITEM'][$i]['blog_id'] = $blog_id;
            $this->BlogList['ITEM'][$i]['user_id'] = $user_id;
            $this->BlogList['ID'][$blog_id] = $BlogInfo['title'];
        }
        
        // return
        return $this->BlogList;
    }
    
    
    function request_feed($blog_id=null, $Q_=array())
    {
        /*
            Request feed for given blog id
            
            Default response will return a two-part array providing CHANNEL or
            blog information and ITEM or post information.
            
            For sample, see:
                http://code.google.com/p/greqo/wiki/SampleFeedArray
        */
        $method = HTTP_REQUEST_METHOD_GET;
        $urlt_ = 'http://www.blogger.com/feeds/%s/posts/default';
        $query_ = '';
                
        // set blog id
        $this->blog_id = ( ! is_null($blog_id) ) ? $blog_id : $this->blog_id;
        $request_url = sprintf($urlt_, $this->blog_id);
        
        // prepare query
        if ( !empty($Q_) ) $query_ = $this->_feed_query($Q_);
                
        // prepare request
        $request_url = $request_url . $query_;
        if ( !$this->_prepare_request($request_url, $method) )
            return 0;
        
        // send request
        $this->_send_client_request();
        
        // check for successful response
        if ( $this->GreqoResponse->code_class != 2 )
            return $this->_trigger_request_error( sprintf(
                'Failed Request [%s]: %s',
                $this->GreqoResponse->code,
                htmlspecialchars($this->GreqoResponse->body)
            ));
            
        // parse response and return
        if ( empty($Q_['alt']) || $Q_['alt'] == 'atom' )
        {
            $FeedArray = $this->atom_to_array($this->GreqoResponse->body);
            foreach ( range(0, count($FeedArray['ITEM'])-1) as $i )
                $FeedArray['ITEM'][$i]['id_num'] = $this->_parse_post_id($FeedArray['ITEM'][$i]['id']);
            return $FeedArray;
        }
        else
            return $this->GreqoResponse->body;    
    }
    
    
    
    /* Write Methods */
    function post_entry($body, $title='', $OptDict=array())
    {
        /*
            Post a new entry to blog.
            
            OptDict can have following items:
                blog_id:    will default to $this->blog_id if not set
                datetime:   set published datetime
                categories: comma-separated category list
                tags:       alias for categories
        */
        
        $method = HTTP_REQUEST_METHOD_POST;
        $urlt_ = 'http://www.blogger.com/feeds/%s/posts/default';
        
        // prepare request url and entry xml
        $Opts = $this->_parse_option_dict($OptDict);
        $this->blog_id = ( ! is_null($Opts['blog_id']) ) ?
            $Opts['blog_id'] : $this->blog_id;
        $request_url = sprintf($urlt_, $this->blog_id);
        $atom_xml = $this->_entry_xml($title, $body, $Opts);
          
        // prepare request
        if ( !$this->_prepare_request($request_url, $method) )
            return 0;
        $this->GreqoClient->addHeader('Content-type', 'application/atom+xml');
        $this->GreqoClient->addRawPostData($atom_xml, 1);
        
        // send request
        if ( !$this->_send_client_request() )
            return 0;  
        
        // check for successful response
        if ( $this->GreqoResponse->code_class != 2 )
            return $this->blogger_error('Blogger Entry Post Failed');
            
        // parse response and return
        if ( empty($Q_['alt']) || $Q_['alt'] == 'atom' )
            return $this->_post_response($this->GreqoResponse->body);            
        else
            return $this->GreqoResponse->body; 
    }
    
    function edit_entry($post_id, $body, $title='', $OptDict=array())
    {
        /*
            Edit an entry with the given post id.
            
            NOTE: this does not follow the recommended Blogger protocol,
            documented here:
            
            http://code.google.com/apis/blogger/docs/2.0/developers_guide_protocol.html#UpdatingEntries
        */
        $method = HTTP_REQUEST_METHOD_PUT;
        $urlt_ = 'http://www.blogger.com/feeds/%s/posts/default/%s';
        
        // prepare request url and entry xml
        $Opts = $this->_parse_option_dict($OptDict);
        $this->blog_id = ( ! is_null($Opts['blog_id']) ) ?
            $Opts['blog_id'] : $this->blog_id;
        $request_url = sprintf($urlt_, $this->blog_id, $post_id);
        $atom_xml = $this->_entry_xml($title, $body, $Opts);
          
        // prepare request
        if ( !$this->_prepare_request($request_url, $method) )
            return 0;
        $this->GreqoClient->addHeader('Content-type', 'application/atom+xml');
        $this->GreqoClient->addRawPostData($atom_xml, 1);
        
        // send request
        $this->_send_client_request();
        
        // check for successful response
        if ( $this->GreqoResponse->code_class != 2 )
            return $this->blogger_error('Blogger Entry Update Failed');
            
        // parse response and return
        if ( empty($Q_['alt']) || $Q_['alt'] == 'atom' )
            return $this->_post_response($this->GreqoResponse->body);
        else
            return $this->GreqoResponse->body; 
    }    
    
    function delete_entry($post_id, $OptDict=array())
    {
        /*
            Delete an Entry
        */
        $method = HTTP_REQUEST_METHOD_DELETE;
        $urlt_ = 'http://www.blogger.com/feeds/%s/posts/default/%s';
        
        // prepare request url and entry xml
        $Opts = $this->_parse_option_dict($OptDict);
        $this->blog_id = ( ! is_null($Opts['blog_id']) ) ?
            $Opts['blog_id'] : $this->blog_id;
        $request_url = sprintf($urlt_, $this->blog_id, $post_id);
          
        // prepare request
        if ( !$this->_prepare_request($request_url, $method) )
            return 0;
        
        // send request
        $this->_send_client_request();
        
        // check for successful response
        if ( $this->GreqoResponse->code_class != 2 )
            return $this->blogger_error("Failed to Delete Blogger Post $post_id");
            
        // success
        return 1;
    }
    
    
    
    /* Comment Methods */
    function post_comment($post_id, $comment, $OptDict=array())
    {
        /*        
            NOTE: comment title and body fields match.
            
            Official documentation:            
            http://code.google.com/apis/blogger/docs/2.0/developers_guide_protocol.html#Comments
        */
        $method = HTTP_REQUEST_METHOD_POST;
        $urlt_ = 'http://www.blogger.com/feeds/%s/%s/comments/default';
        
        // prepare request url and entry xml
        $Opts = $this->_parse_option_dict($OptDict);
        $this->blog_id = ( ! is_null($Opts['blog_id']) ) ?
            $Opts['blog_id'] : $this->blog_id;
        $request_url = sprintf($urlt_, $this->blog_id, $post_id);
        $atom_xml = $this->_comment_xml($comment, $Opts);
          
        // prepare request
        if ( !$this->_prepare_request($request_url, $method) )
            return 0;
        $this->GreqoClient->addHeader('Content-type', 'application/atom+xml');
        $this->GreqoClient->addRawPostData($atom_xml, 1);
        
        // send request
        $this->_send_client_request();
        
        // check for successful response
        if ( $this->GreqoResponse->code_class != 2 )
            return $this->blogger_error('Blogger Comment Failed');
            
        // parse response and return
        if ( empty($Q_['alt']) || $Q_['alt'] == 'atom' )
            return $this->_post_response($this->GreqoResponse->body);
        else
            return $this->GreqoResponse->body; 
    }
    
    function delete_comment($comment_id, $post_id, $OptDict=array())
    {
        /*
            Delete an Entry
        */
        $method = HTTP_REQUEST_METHOD_DELETE;
        $urlt_ = 'http://www.blogger.com/feeds/%s/%s/comments/default/%s';
        
        // prepare request url and entry xml
        $Opts = $this->_parse_option_dict($OptDict);
        $this->blog_id = ( ! is_null($Opts['blog_id']) ) ?
            $Opts['blog_id'] : $this->blog_id;
        $request_url = sprintf($urlt_, $this->blog_id, $post_id, $comment_id);
          
        // prepare request
        if ( !$this->_prepare_request($request_url, $method) )
            return 0;
        
        // send request
        $this->_send_client_request();
        
        // check for successful response
        if ( $this->GreqoResponse->code_class != 2 )
            return $this->blogger_error("Failed to Delete Blogger Comment $comment_id");
            
        // success
        return 1;
    }
    
    function get_comments($blog_id=null)
    {
        /*
            Get all comments for a blog.
            
            Official documentation:            
            http://code.google.com/apis/blogger/docs/2.0/developers_guide_protocol.html#Comments
        */
        $method = HTTP_REQUEST_METHOD_GET;
        $urlt_ = 'http://www.blogger.com/feeds/%s/comments/default';
        
        // prepare request url and entry xml
        $this->blog_id = ( ! is_null($blog_id) ) ? $blog_id : $this->blog_id;
        $request_url = sprintf($urlt_, $this->blog_id);
          
        // prepare request
        if ( !$this->_prepare_request($request_url, $method) )
            return 0;
        
        // send request
        $this->_send_client_request();
        
        // check for successful response
        if ( $this->GreqoResponse->code_class != 2 )
            return $this->blogger_error("Unabled to retrieve comments for blog #{$this->blog_id}");
            
        // parse response and return
        if ( empty($Q_['alt']) || $Q_['alt'] == 'atom' )
        {
            $CommentArray = $this->atom_to_array($this->GreqoResponse->body);
            foreach ( range(0, count($CommentArray['ITEM'])-1) as $i )
                $CommentArray['ITEM'][$i]['id_num'] = $this->_parse_post_id($CommentArray['ITEM'][$i]['id']);
            return $CommentArray;
        }
        else
            return $this->GreqoResponse->body; 
    }
    
    function get_comments_by_post($post_id, $blog_id=null)
    {
        /*
            Get comments for a specific post.
            
            Official documentation:            
            http://code.google.com/apis/blogger/docs/2.0/developers_guide_protocol.html#Comments
        */        
        $method = HTTP_REQUEST_METHOD_GET;
        $urlt_ = 'http://www.blogger.com/feeds/%s/%s/comments/default';
        
        // prepare request url and entry xml
        $this->blog_id = ( ! is_null($blog_id) ) ? $blog_id : $this->blog_id;
        $request_url = sprintf($urlt_, $this->blog_id, $post_id);
          
        // prepare request
        if ( !$this->_prepare_request($request_url, $method) )
            return 0;
        
        // send request
        $this->_send_client_request();
        
        // check for successful response
        if ( $this->GreqoResponse->code_class != 2 )
            return $this->blogger_error("Unabled to retrieve comments for blog #{$this->blog_id}");
            
        // parse response and return
        if ( empty($Q_['alt']) || $Q_['alt'] == 'atom' )
        {
            $CommentArray = $this->atom_to_array($this->GreqoResponse->body);
            foreach ( range(0, count($CommentArray['ITEM'])-1) as $i )
                $CommentArray['ITEM'][$i]['id_num'] = $this->_parse_post_id($CommentArray['ITEM'][$i]['id']);
            return $CommentArray;
        }
        else
            return $this->GreqoResponse->body;
    }
    
    
    
    function blogger_error($m='Blogger Error')
    {
        $et_ = "%s [%s]: %s";
        $code = ( !empty($this->GreqoResponse->code) ) ?
            $this->GreqoResponse->code : 'non-200 code (?)';
        $response = ( !empty($this->GreqoResponse->body) ) ?
            htmlspecialchars($this->GreqoResponse->body) : 'no response';
        
        $e = sprintf($et_, $m, $code, $response);
        return $this->_trigger_request_error($e);
    }
    
    
    
    /* ** PRIVATE METHODS ** */
    function _post_response($xml_string)
    {
        $GoogleResponse = array(
            'id'        => 0,
            'blog_num'  => 0,
            'id_num'    => 0,
            'published' => '',
            'code'      => $this->GreqoResponse->code,
            'links'     => array(),
            'body'      => $xml_string            
        );
        
        // get links
        $link_regex = "%<link\s*rel='([^']*)'\s*type='([^']*)'\s*href='([^']*)'%i";
        if ( preg_match_all($link_regex, $xml_string, $LinkMatch) )
        {
            if ( isset($LinkMatch[0][0]) ) $LinkMatch[4]['replies'] = $LinkMatch[3][0];
            if ( isset($LinkMatch[0][0]) ) $LinkMatch[4]['edit'] = $LinkMatch[3][2];
            if ( isset($LinkMatch[0][0]) ) $LinkMatch[4]['self'] = $LinkMatch[3][3];
            if ( isset($LinkMatch[0][0]) ) $LinkMatch[4]['alternate'] = $LinkMatch[3][4];
            if ( isset($LinkMatch[0][0]) ) $LinkMatch[4]['view'] = $LinkMatch[3][4];
            $GoogleResponse['links'] = $LinkMatch[4];
        }
        
        // get published
        $pub_regex = '%<published.*>(.*)<%iU';
        if ( preg_match_all($pub_regex, $xml_string, $PubMatch) )
            $GoogleResponse['published'] = $PubMatch[1][0];
        
        // get id and id_num
        $id_regex = '%<id.*>(.*)<%iU';
        if ( preg_match_all($id_regex, $xml_string, $IdMatch) )
        {
            $GoogleResponse['id'] = $IdMatch[1][0];
            $IdSplit1 = explode('.post-', $GoogleResponse['id']);
            if ( isset($IdSplit1[1]) ) $GoogleResponse['id_num'] = $IdSplit1[1];
            $IdSplit2 = explode('blog-', $IdSplit1[0]);
            $GoogleResponse['blog_num'] = $IdSplit2[1];
        }
    
        return $GoogleResponse;
    }
    
    function _entry_xml($title, $body, $Options)
    {
        // post template
        $atomt_ = <<<XML
<entry xmlns='http://www.w3.org/2005/Atom'>
    <title type='%s'>%s</title>
    %s
    <content type='%s'>%s</content>
    %s
</entry>
XML;
        
        $atom_xml = sprintf( $atomt_,
            $this->content_type,
            $this->escape_entities(trim($title)),
            $this->_entry_published($Options['datetime']),
            $this->content_type,
            $this->escape_entities(trim($body)),
            $this->_entry_categories($Options['categories']) );
        
        return $atom_xml;
    }
    
    function _comment_xml($comment, $Options=array())
    {
        /*
            NOTE: title and body match
        */
        
        // template
        $atomt_ = <<<XML
<entry xmlns='http://www.w3.org/2005/Atom'>
    <title type='text'>%s</title>
    <content type='html'>%s</content>
</entry>
XML;
        
        $comment_ = $this->escape_entities(trim($comment));
        $atom_xml = sprintf($atomt_, $comment_, $comment_);    
        return $atom_xml;
    }
    
    function _entry_published($datetime)
    {
        $tagt_ = '<published>%s</published>';
        
        if ( empty($datetime) ) return '';
        return sprintf($tagt_, $datetime);
    }
    
    function _entry_categories($CategoryList)
    {
        $category_str = '';
        $tagt_ = '<category scheme="http://www.blogger.com/atom/ns#" term="%s" />';
        
        if ( empty($CategoryList) ) return '';
        
        if ( !is_array($CategoryList) )
            $CategoryList = explode(',', $CategoryList);
            
        foreach ( $CategoryList as $cat_ )
        {
            $cat_ = trim($this->escape_entities($cat_));
            $category_str .= sprintf($tagt_, $cat_) . "\n";
        }
        
        return $category_str;        
    }
    
    function _parse_option_dict($OptDict)
    {
        $Opts = array();    // return
        
        // simple settings
        $Opts['blog_id'] = ( !empty($OptDict['blog_id']) ) ?
            $OptDict['blog_id'] : null;
        $Opts['datetime'] = ( !empty($OptDict['datetime']) ) ?
            $this->rfc3339($OptDict['datetime']) : null;
        
        // categories/tags
        if ( isset($OptDict['tags']) && empty($OptDict['categories']) )
            $OptDict['categories'] = $OptDict['tags'];
        $Opts['categories'] = ( !empty($OptDict['categories']) ) ?
            $OptDict['categories'] : null;
        
        return $Opts;
    }
    
    function _feed_query($Q_)
    {
        /* see reference above */
        $query = '';            // return
        $Q_Dict = array();
        
        // date fields
        $DateFields = array(
            'published-min', 'published-max',
            'updated-min', 'updated-max' );
        
        // alt        
        if ( !empty($Q_['alt']) ) $Q_Dict['alt'] = $Q_['alt'];

        // category
        if ( !empty($Q_['category']) ) {
            
            // normalize input
            if ( !is_array($Q_['category'])
                && ( strpos($Q_['category'], ',') !== false) )
                $Q_['category'] = explode(',', $Q_['category']);
            elseif ( !is_array($Q_['category']) )
                $Q_['category'] = array( $Q_['category'] );

            // extend dict
            if ( isset($Q_['category']['or']) )
                $Q_Dict['category'] = implode('|', $Q_['category']['or']);
            elseif ( isset($Q_['category']['and']) )
                $Q_Dict['category'] = implode(',', $Q_['category']['and']);
            else
                $Q_Dict['category'] = implode(',', $Q_['category']);
        }
        
        // start-index, max-results
        if ( !empty($Q_['start-index']) ) $Q_Dict['start-index'] = $Q_['start-index'];
        if ( !empty($Q_['offset']) ) $Q_Dict['start-index'] = $Q_['offset'];
        if ( !empty($Q_['max-results']) ) $Q_Dict['max-results'] = $Q_['max-results'];
        if ( !empty($Q_['limit']) ) $Q_Dict['max-results'] = $Q_['limit'];
        
        // orderby
        if ( !empty($Q_['orderby']) ) $Q_Dict['orderby'] = $Q_['orderby'];
        
        // published-min, published-max
        if ( isset($Q_['between']) && is_array($Q_['between']) ) {
            if ( isset($Q_['between'][0]) ) $Q_Dict['published-min'] = $Q_['between'][0];
            if ( isset($Q_['between'][1]) ) $Q_Dict['published-max'] = $Q_['between'][1];
        }
        if ( !empty($Q_['published-min']) ) $Q_Dict['published-min'] = $Q_['published-min'];
        if ( !empty($Q_['published-max']) ) $Q_Dict['published-max'] = $Q_['published-max'];
        
        // updated-min, updated-max
        if ( !empty($Q_['updated-min']) ) $Q_Dict['updated-min'] = $Q_['updated-min'];
        if ( !empty($Q_['updated-max']) ) $Q_Dict['updated-max'] = $Q_['updated-max'];

        // glue together query
        if ( !empty($Q_Dict) ) $query = '?';
        foreach ( $Q_Dict as $qkey => $qval )
        {
            // convert datestamps
            // note: strtotime return -1 in PHP4, false in PHP5 on fail
            if ( in_array($qkey, $DateFields) && (int)strtotime($qval) > 0 )
                $qval = $this->get_datestamp(strtotime($qval));
            elseif ( in_array($qkey, $DateFields) )
                $qval = $this->get_datestamp($qval);
            
            $query .= sprintf('%s=%s&', urlencode($qkey), urlencode($qval));
        }
        if ( !empty($query) ) $query = substr($query, 0, -1);
        
        return $query;
    }
    
    function _prepare_request($request_url, $method)
    {
        // login check
        if ( !$this->_is_login_prepared() ) return 0;
        
        // get authorization token (will raise error if unsuccessful)
        if ( !$auth_token = $this->request_clientlogin_token($this->service, $force=0) ) 
           return 0;
        
        // set client url and headers
        $this->GreqoClient->setURL($request_url);
        $this->GreqoClient->setMethod($method);
        $this->GreqoClient->addHeader('Authorization', 'GoogleLogin auth=' . $auth_token);
        $this->GreqoClient->addHeader('GData-Version', $this->gdata_version);
        
        return 1;
    }
    
    function _parse_blog_id($id_string)
    {
        if ( !preg_match_all('%.*user-(\d+)\.blog-(\d+)$%iU', $id_string, $Matches) )
        {
            trigger_error("unable to parse blog id from id string: $id_string",
                E_USER_WARNING);
            return 0;
        }
        
        return $Matches[2][0];
    }
    
    function _parse_user_id($id_string)
    {
        if ( !preg_match_all('%.*user-(\d+)\.blog-(\d+)$%iU', $id_string, $Matches) )
        {
            trigger_error("unable to parse blog id from id string: $id_string",
                E_USER_WARNING);
            return 0;
        }
        
        return $Matches[1][0];
    }
    
    function _parse_post_id($id_string)
    {
        $IdSplit = explode('.post-', $id_string);
        
        if ( !isset($IdSplit[1]) )
        {
            trigger_error("unable to parse post id from id string: $id_string",
                E_USER_WARNING);
            return 0;
        }
        
        return $IdSplit[1];
    }

    function _get_filename() { return basename(__FILE__); }
    function _get_dirpath() { return dirname(__FILE__) . $this->DS; }

} // end class

?>
